package crmdna.inventory;

import static crmdna.common.TestUtil.ensureAPIException;
import static crmdna.common.TestUtil.ensureResourceIncorrectException;
import static crmdna.common.TestUtil.ensureResourceNotFoundException;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;

import java.util.Date;
import java.util.List;

import org.junit.After;
import org.junit.Before;
import org.junit.Test;

import com.google.appengine.tools.development.testing.LocalDatastoreServiceTestConfig;
import com.google.appengine.tools.development.testing.LocalServiceTestHelper;
import com.googlecode.objectify.ObjectifyFilter;

import crmdna.client.Client;
import crmdna.common.ICode;
import crmdna.common.UnitUtils.PhysicalQuantity;
import crmdna.common.UnitUtils.ReportingUnit;
import crmdna.common.Utils;
import crmdna.common.Utils.Currency;
import crmdna.common.api.APIResponse.Status;
import crmdna.group.Group;
import crmdna.group.Group.GroupProp;
import crmdna.hr.Department;
import crmdna.hr.DepartmentProp;
import crmdna.inventory.InventoryItemCore.CheckInOrOut;
import crmdna.user.User;
import crmdna.user.User.Action;
import crmdna.user.User.ResourceType;
import crmdna.user.UserCore;

public class InventoryItemTest {
  private final LocalServiceTestHelper datastoreHelper = new LocalServiceTestHelper(
      new LocalDatastoreServiceTestConfig().setApplyAllHighRepJobPolicy());

  private final String client = "isha";
  private final String validUser = "valid@login.com";
  private final String userWithPermission = "withpermission@login.com";
  private GroupProp sgp;
  private GroupProp kl;
  InventoryItemTypeProp book;
  InventoryItemTypeProp rudraksh;

  DepartmentProp nandiFoods;

  @Before
  public void setUp() {
    datastoreHelper.setUp();
    ObjectifyFilter.complete();

    Client.create(client);
    sgp = Group.create(client, "Singapore", User.SUPER_USER);
    assertEquals(1, sgp.groupId);

    kl = Group.create(client, "KL", User.SUPER_USER);
    assertEquals(2, kl.groupId);

    book = InventoryItemType.create(client, "Book", User.SUPER_USER);
    assertEquals(1, book.inventoryItemTypeId);

    rudraksh = InventoryItemType.create(client, "Rudraksh", User.SUPER_USER);
    assertEquals(2, rudraksh.inventoryItemTypeId);

    User.create(client, validUser, sgp.groupId, User.SUPER_USER);
    assertEquals(1, UserCore.get(client, validUser).toProp().userId);

    User.create(client, userWithPermission, sgp.groupId, User.SUPER_USER);
    assertEquals(2, UserCore.get(client, userWithPermission).toProp().userId);
    User.addOrDeletePrivilege(client, userWithPermission, ResourceType.INVENTORY_ITEM, 1,
        Action.WRITE, true, User.SUPER_USER);
    User.addOrDeletePrivilege(client, userWithPermission, ResourceType.INVENTORY_ITEM, 1,
        Action.UPDATE_QUANTITY, true, User.SUPER_USER);

    nandiFoods = Department.create(client, "Nandi Foods", User.SUPER_USER);
  }

  @After
  public void tearDown() {
    ObjectifyFilter.complete();
    datastoreHelper.tearDown();
  }

  @Test
  public void createTest() {
    InventoryItemProp forestFlower =
        InventoryItem.create(client, sgp.groupId, book.inventoryItemTypeId, "Forest Flower",
            PhysicalQuantity.NUMBER, ReportingUnit.NUMBER, User.SUPER_USER);

    assertEquals(1, forestFlower.inventoryItemId);

    ObjectifyFilter.complete();

    forestFlower = InventoryItem.safeGet(client, forestFlower.inventoryItemId).toProp();
    assertEquals(1, forestFlower.inventoryItemId);
    assertEquals(sgp.groupId, forestFlower.groupId);
    assertEquals(book.inventoryItemTypeId, forestFlower.inventoryItemTypeId);
    assertEquals("Forest Flower", forestFlower.displayName);

    // name should be populated correctly in the entity
    assertEquals("forestflower", InventoryItem.safeGet(client, forestFlower.inventoryItemId).name);

    assertEquals(PhysicalQuantity.NUMBER, forestFlower.physicalQuantity);
    assertEquals(ReportingUnit.NUMBER, forestFlower.reportingUnit);

    // client cannot be invalid
    ensureResourceNotFoundException(new ICode() {
      @Override
      public void run() {
        InventoryItem.create("invalid", sgp.groupId, book.inventoryItemTypeId, "joy 24/7",
            PhysicalQuantity.NUMBER, ReportingUnit.NUMBER, User.SUPER_USER);
      }
    });

    // inventory item type cannot be invalid
    ensureResourceNotFoundException(new ICode() {
      @Override
      public void run() {
        InventoryItem.create(client, sgp.groupId, 100, "Himalayas", PhysicalQuantity.NUMBER,
            ReportingUnit.NUMBER, User.SUPER_USER);
      }
    });

    // same name cannot repeat within a group
    ensureAPIException(Status.ERROR_RESOURCE_ALREADY_EXISTS, new ICode() {

      @Override
      public void run() {
        InventoryItem.create(client, sgp.groupId, book.inventoryItemTypeId, "Forest Flower",
            PhysicalQuantity.NUMBER, ReportingUnit.NUMBER, User.SUPER_USER);
      }
    });

    // but same name can repeat within another group
    forestFlower =
        InventoryItem.create(client, kl.groupId, book.inventoryItemTypeId, "Forest Flower",
            PhysicalQuantity.NUMBER, ReportingUnit.NUMBER, User.SUPER_USER);
    assertEquals(kl.groupId, forestFlower.groupId);

    // need permission
    ensureAPIException(Status.ERROR_INSUFFICIENT_PERMISSION, new ICode() {

      @Override
      public void run() {
        InventoryItem.create(client, kl.groupId, rudraksh.inventoryItemTypeId, "Gowri Shankar",
            PhysicalQuantity.NUMBER, ReportingUnit.NUMBER, validUser);
      }
    });

    // user with permission for sgp cannot create for kl
    ensureAPIException(Status.ERROR_INSUFFICIENT_PERMISSION, new ICode() {

      @Override
      public void run() {
        InventoryItem.create(client, kl.groupId, rudraksh.inventoryItemTypeId, "Gowri Shankar",
            PhysicalQuantity.NUMBER, ReportingUnit.NUMBER, userWithPermission);
      }
    });

    // user with permssion can create in singapore
    InventoryItem.create(client, sgp.groupId, rudraksh.inventoryItemTypeId, "Gowri Shankar",
        PhysicalQuantity.NUMBER, ReportingUnit.NUMBER, userWithPermission);
    // no exception
  }

  @Test
  public void updateTest() {
    InventoryItemProp forestFlower =
        InventoryItem.create(client, sgp.groupId, rudraksh.inventoryItemTypeId, "Flower Forest",
            PhysicalQuantity.NUMBER, ReportingUnit.NUMBER, User.SUPER_USER);

    // create another book - mystic eye
    InventoryItem.create(client, sgp.groupId, rudraksh.inventoryItemTypeId, "Mystic Eye",
        PhysicalQuantity.NUMBER, ReportingUnit.NUMBER, User.SUPER_USER);

    assertEquals(1, forestFlower.inventoryItemId);

    // change name to Flower-Forest
    forestFlower =
        InventoryItem.update(client, forestFlower.inventoryItemId, book.inventoryItemTypeId,
            "Flower-Forest", null, userWithPermission);

    assertEquals(forestFlower.inventoryItemTypeId, book.inventoryItemTypeId);
    assertEquals("Flower-Forest", forestFlower.displayName);

    // change name to forest-flower
    forestFlower =
        InventoryItem.update(client, forestFlower.inventoryItemId, book.inventoryItemTypeId,
            "Forest-Flower", null, userWithPermission);

    assertEquals(forestFlower.inventoryItemTypeId, book.inventoryItemTypeId);
    assertEquals("Forest-Flower", forestFlower.displayName);
    assertEquals("forestflower", forestFlower.name);

    // new reporting unit should be valid
    final long inventoryItemId = forestFlower.inventoryItemId;
    ensureResourceIncorrectException(new ICode() {

      @Override
      public void run() {
        InventoryItem.update(client, inventoryItemId, book.inventoryItemTypeId, null,
            ReportingUnit.ML, userWithPermission);
      }
    });

    // need permission for updating
    ensureAPIException(Status.ERROR_INSUFFICIENT_PERMISSION, new ICode() {

      @Override
      public void run() {
        InventoryItem.update(client, inventoryItemId, book.inventoryItemTypeId, null,
            ReportingUnit.ML, validUser);
      }
    });

    // first char of display name should be alphanumeric
    ensureResourceIncorrectException(new ICode() {

      @Override
      public void run() {
        InventoryItem.update(client, inventoryItemId, book.inventoryItemTypeId, "$Forest Flower",
            ReportingUnit.ML, userWithPermission);
      }
    });

    // cannot change name to already existing mystic eye
    ensureAPIException(Status.ERROR_RESOURCE_ALREADY_EXISTS, new ICode() {

      @Override
      public void run() {
        InventoryItem.update(client, inventoryItemId, book.inventoryItemTypeId, "mystic-Eye",
            ReportingUnit.ML, userWithPermission);
      }
    });
  }

  @Test
  public void queryTest() {
    InventoryItemProp forestFlower =
        InventoryItem.create(client, sgp.groupId, book.inventoryItemTypeId, "Forest Flower",
            PhysicalQuantity.NUMBER, ReportingUnit.NUMBER, User.SUPER_USER);
    assertEquals(1, forestFlower.inventoryItemId);

    InventoryItemProp mysticEye =
        InventoryItem.create(client, sgp.groupId, book.inventoryItemTypeId, "Mystic Eye",
            PhysicalQuantity.NUMBER, ReportingUnit.NUMBER, User.SUPER_USER);
    assertEquals(2, mysticEye.inventoryItemId);

    InventoryItemProp midnights =
        InventoryItem.create(client, kl.groupId, book.inventoryItemTypeId, "Midnights with Mystic",
            PhysicalQuantity.NUMBER, ReportingUnit.NUMBER, User.SUPER_USER);
    assertEquals(3, midnights.inventoryItemId);

    InventoryItemProp panchamuki =
        InventoryItem.create(client, sgp.groupId, rudraksh.inventoryItemTypeId, "Panchamuki",
            PhysicalQuantity.NUMBER, ReportingUnit.NUMBER, User.SUPER_USER);
    assertEquals(4, panchamuki.inventoryItemId);

    InventoryItemQueryCondition qc = new InventoryItemQueryCondition();
    qc.groupId = sgp.groupId;
    List<InventoryItemProp> props = InventoryItem.query(client, qc, User.SUPER_USER);

    assertEquals(3, props.size());
    // should be sorted by inventory type first and then by item name
    assertEquals(forestFlower.inventoryItemId, props.get(0).inventoryItemId);
    assertEquals(mysticEye.inventoryItemId, props.get(1).inventoryItemId);
    assertEquals(panchamuki.inventoryItemId, props.get(2).inventoryItemId);

    // query for all books
    qc = new InventoryItemQueryCondition();
    qc.inventoryItemTypeIds = Utils.getSet(book.inventoryItemTypeId);

    props = InventoryItem.query(client, qc, validUser);

    assertEquals(3, props.size());
    // should be sorted by name
    assertEquals(forestFlower.inventoryItemId, props.get(0).inventoryItemId);
    assertEquals(midnights.inventoryItemId, props.get(1).inventoryItemId);
    assertEquals(mysticEye.inventoryItemId, props.get(2).inventoryItemId);

    // query for books and rudraksh
    qc = new InventoryItemQueryCondition();
    qc.inventoryItemTypeIds.add(book.inventoryItemTypeId);
    qc.inventoryItemTypeIds.add(rudraksh.inventoryItemTypeId);
    props = InventoryItem.query(client, qc, validUser);
    assertEquals(4, props.size());

    // should be sorted by inventory type first and then by item name
    assertEquals(forestFlower.inventoryItemId, props.get(0).inventoryItemId);
    assertEquals(midnights.inventoryItemId, props.get(1).inventoryItemId);
    assertEquals(mysticEye.inventoryItemId, props.get(2).inventoryItemId);
    assertEquals(panchamuki.inventoryItemId, props.get(3).inventoryItemId);

    // query by first char
    qc = new InventoryItemQueryCondition();
    qc.firstChars = Utils.getList("m");
    qc.groupId = sgp.groupId;
    props = InventoryItem.query(client, qc, validUser);
    assertEquals(1, props.size());

    // query for everything in KL
    qc = new InventoryItemQueryCondition();
    qc.groupId = kl.groupId;
    props = InventoryItem.query(client, qc, validUser);
    assertEquals(1, props.size());
    assertEquals(midnights.inventoryItemId, props.get(0).inventoryItemId);

    // check in 5 midnights book at 200 MYR per book
    InventoryItem.checkIn(client, midnights.inventoryItemId, 5, ReportingUnit.NUMBER, 200,
        Currency.MYR, "5 @ 200 MYR / Book", User.SUPER_USER);
    props = InventoryItem.query(client, qc, validUser);
    assertEquals(1, props.size());
    assertEquals(midnights.inventoryItemId, props.get(0).inventoryItemId);
    assertTrue(5.0 == props.get(0).availableQtyInReportingUnit);
    assertTrue(200 == props.get(0).avgPricePerReportingUnit);
    assertEquals(Currency.MYR, props.get(0).ccy);

    // check in 5 more at 300 MYR per book
    InventoryItem.checkIn(client, midnights.inventoryItemId, 5, ReportingUnit.NUMBER, 300,
        Currency.MYR, "5 @ 300 MYR / Book", User.SUPER_USER);
    props = InventoryItem.query(client, qc, validUser);
    assertEquals(1, props.size());
    assertEquals(midnights.inventoryItemId, props.get(0).inventoryItemId);
    assertTrue(10 == props.get(0).availableQtyInReportingUnit);
    assertTrue(250 == props.get(0).avgPricePerReportingUnit);
    assertEquals(Currency.MYR, props.get(0).ccy);

    // check out 3 books
    InventoryItem.checkOut(client, midnights.inventoryItemId, 3, ReportingUnit.NUMBER, null, null,
        "check out 3 books", null, User.SUPER_USER);
    props = InventoryItem.query(client, qc, validUser);
    assertEquals(1, props.size());
    assertEquals(midnights.inventoryItemId, props.get(0).inventoryItemId);
    assertTrue(7 == props.get(0).availableQtyInReportingUnit);
    assertTrue(Math.abs(271.42 - props.get(0).avgPricePerReportingUnit) < 0.1);
    // (2 * 200 + 5 * 300)/7 = 271.42
    assertEquals(Currency.MYR, props.get(0).ccy);

    // check out 3 more books
    InventoryItem.checkOut(client, midnights.inventoryItemId, 3, ReportingUnit.NUMBER, null, null,
        "check out 3 books", null, User.SUPER_USER);
    props = InventoryItem.query(client, qc, validUser);
    assertEquals(1, props.size());
    assertEquals(midnights.inventoryItemId, props.get(0).inventoryItemId);
    assertTrue(4 == props.get(0).availableQtyInReportingUnit);
    assertTrue(300 == props.get(0).avgPricePerReportingUnit);
    assertEquals(Currency.MYR, props.get(0).ccy);
  }

  @Test
  public void queryStockChangesTest() {
    final InventoryItemProp forestFlower =
        InventoryItem.create(client, sgp.groupId, book.inventoryItemTypeId, "Forest Flower",
            PhysicalQuantity.NUMBER, ReportingUnit.NUMBER, User.SUPER_USER);

    assertEquals(1, forestFlower.inventoryItemId);

    InventoryItem.checkIn(client, forestFlower.inventoryItemId, 200, ReportingUnit.NUMBER, 1,
        Currency.SGD, "200 @ 1 SGD / Book", userWithPermission);

    StockChangeQueryCondition qc =
        new StockChangeQueryCondition(sgp.groupId, new Date().getTime() - 200,
            new Date().getTime() + 100);
    qc.includeCheckIn = true;
    List<StockChangeProp> props = InventoryItem.queryStockChanges(client, qc, userWithPermission);
    assertEquals(1, props.size());
    assertEquals(Currency.SGD, props.get(0).ccy);
    assertTrue(200.0 == props.get(0).changeInReportingUnit);
    assertEquals(CheckInOrOut.CHECK_IN, props.get(0).checkInOrOut);
    assertEquals(userWithPermission, props.get(0).login);
    assertEquals(forestFlower.inventoryItemId, props.get(0).inventoryItemId);
    assertEquals("Forest Flower", props.get(0).inventoryItem);
    assertEquals(1, props.get(0).checkInOrOutId);

    // query based on login
    qc.logins.add(validUser);
    props = InventoryItem.queryStockChanges(client, qc, userWithPermission);
    assertEquals(0, props.size());

    qc.logins.add(userWithPermission);
    props = InventoryItem.queryStockChanges(client, qc, userWithPermission);
    assertEquals(1, props.size());

    // include only checkouts
    qc.includeCheckIn = false;
    qc.includeCheckOut = true;
    props = InventoryItem.queryStockChanges(client, qc, userWithPermission);
    assertEquals(0, props.size());

    InventoryItem.checkOut(client, forestFlower.inventoryItemId, 100, ReportingUnit.NUMBER, 1.2,
        Currency.SGD, null, Utils.getSet("tag1"), userWithPermission);
    props = InventoryItem.queryStockChanges(client, qc, userWithPermission);
    assertEquals(1, props.size());
    assertEquals(1, props.get(0).tags.size());
    assertTrue(props.get(0).tags.contains("tag1"));
  }

  @Test
  public void tagsSavedDuringCheckout() {
    final InventoryItemProp forestFlower =
        InventoryItem.create(client, sgp.groupId, book.inventoryItemTypeId, "Forest Flower",
            PhysicalQuantity.NUMBER, ReportingUnit.NUMBER, User.SUPER_USER);

    assertEquals(1, forestFlower.inventoryItemId);

    InventoryItem.checkIn(client, forestFlower.inventoryItemId, 200, ReportingUnit.NUMBER, 1,
        Currency.SGD, "200 @ 1 SGD / Book", userWithPermission);

    InventoryCheckOutProp inventoryCheckOutProp =
        InventoryItem.checkOut(client, forestFlower.inventoryItemId, 100, ReportingUnit.NUMBER,
            1.2, Currency.SGD, null, Utils.getSet("tag1"), userWithPermission);
    assertEquals(1, inventoryCheckOutProp.tags.size());
    assertTrue(inventoryCheckOutProp.tags.contains("tag1"));
  }

  @Test
  public void checkInTest() {
    final InventoryItemProp forestFlower =
        InventoryItem.create(client, sgp.groupId, book.inventoryItemTypeId, "Forest Flower",
            PhysicalQuantity.NUMBER, ReportingUnit.NUMBER, User.SUPER_USER);

    assertEquals(1, forestFlower.inventoryItemId);

    // permission required for check in
    ensureAPIException(Status.ERROR_INSUFFICIENT_PERMISSION, new ICode() {

      @Override
      public void run() {
        InventoryItem.checkIn(client, forestFlower.inventoryItemId, 200, ReportingUnit.NUMBER, 1,
            Currency.SGD, "200 @ 1 SGD / Book", validUser);
      }
    });

    long ms = new Date().getTime();

    InventoryCheckInProp prop =
        InventoryItem.checkIn(client, forestFlower.inventoryItemId, 200, ReportingUnit.NUMBER, 1,
            Currency.SGD, "200 @ 1 SGD / Book", userWithPermission);
    assertEquals(1, prop.checkInId);
    assertTrue(Math.abs(prop.timestamp.getTime() - ms) < 10);

    assertEquals(true, prop.available);
    assertTrue(200.0 == prop.availableQtyInDefaultUnit);
    assertEquals(Currency.SGD, prop.ccy);
    assertEquals(ReportingUnit.NUMBER, prop.defaultUnit);
    assertTrue(1.0 == prop.pricePerDefaultUnit);
    assertTrue(200.0 == prop.qtyInDefaultUnit);
    assertEquals(prop.login, userWithPermission);

    InventoryItemProp inventoryItemProp =
        InventoryItem.safeGet(client, forestFlower.inventoryItemId).toProp();
    InventoryItemProp.populateDependents(client, Utils.getList(inventoryItemProp));
    assertTrue(200 == inventoryItemProp.availableQtyInReportingUnit);
    assertEquals(Currency.SGD, inventoryItemProp.ccy);
    assertTrue(1 == inventoryItemProp.avgPricePerReportingUnit);

    InventoryItem.checkIn(client, forestFlower.inventoryItemId, 200, ReportingUnit.NUMBER, 2,
        Currency.SGD, "200 @ 1 SGD / Book", userWithPermission);

    inventoryItemProp = InventoryItem.safeGet(client, forestFlower.inventoryItemId).toProp();
    InventoryItemProp.populateDependents(client, Utils.getList(inventoryItemProp));
    assertTrue(400 == inventoryItemProp.availableQtyInReportingUnit);
    assertEquals(Currency.SGD, inventoryItemProp.ccy);
    assertTrue(1.5 == inventoryItemProp.avgPricePerReportingUnit);
  }

  @Test
  public void checkOutTest() {
    final InventoryItemProp forestFlower =
        InventoryItem.create(client, sgp.groupId, book.inventoryItemTypeId, "Forest Flower",
            PhysicalQuantity.NUMBER, ReportingUnit.NUMBER, User.SUPER_USER);

    assertEquals(1, forestFlower.inventoryItemId);

    // check in 200 forest flowers @ 1 SGD per book
    InventoryItem.checkIn(client, forestFlower.inventoryItemId, 200, ReportingUnit.NUMBER, 1,
        Currency.SGD, "200 @ 1 SGD / Book", userWithPermission);

    // check in 200 more @ 2 SGD per book
    InventoryItem.checkIn(client, forestFlower.inventoryItemId, 200, ReportingUnit.NUMBER, 2,
        Currency.SGD, "200 @ 1 SGD / Book", userWithPermission);

    // check out 300 without specifying price
    InventoryCheckOutProp checkOutProp =
        InventoryItem.checkOut(client, forestFlower.inventoryItemId, 300, ReportingUnit.NUMBER,
            null, Currency.SGD, "checking out 300", null, userWithPermission);

    assertTrue(Math.abs(1.3333 - checkOutProp.avgPricePerDefaultUnit) < 0.01);
    // (200 * 1 + 100 * 2)/(100 + 200) = 133.33
  }
}
